package engine;

import java.util.HashMap;
import java.util.Vector;
import java.util.concurrent.ExecutionException;

import engine.math.Arifmetic;
import engine.math.MT19937;
import engine.utils.StructXY;

public class StarMap {
	private Sector[][] coordinateNet;

	/**
	 * Конструктор звездной карты для случайного заполнения
	 * 
	 * @param mapSize размер карты
	 * @param numberOfStarSystems число звездных систем, которое нужно создать
	 */
	protected StarMap(int mapSize, int numberOfStarSystems) {
		this.coordinateNet = new Sector[mapSize][mapSize];
		this.randomUniverse(numberOfStarSystems, this.generateEmptyMap(new StructXY(0, 0), mapSize));
	}

	/**
	 * Конструктор звездной карты для кластерного заполнения
	 * 
	 * @param mapSize размер карты
	 * @param numberOfStarSystems число звездных систем, которое нужно создать
	 * @param numberOfClusters число кластеров на которое разбивается карта
	 */
	protected StarMap(int mapSize, int numberOfStarSystems, int numberOfClusters) {
		this.coordinateNet = new Sector[mapSize][mapSize];
		this.randomUniverse(numberOfStarSystems, numberOfClusters);
		this.generateEmptyMap(new StructXY(0, 0), mapSize);
	}

	/**
	 * Конструктор звездной карты для спирального заполнения
	 * 
	 * @param mapSize размер карты
	 */
	protected StarMap(int mapSize) {
		boolean accept = true;
		Vector<HashMap<Integer, StructXY>> clusterLine = null;
		this.coordinateNet = new Sector[mapSize][mapSize];

		int starQuant = mapSize;
		while (starQuant % 4 != 0) {
			starQuant--;
		}

		try {
			clusterLine = this.clusterize(4, starQuant);
		} catch (CantCreateException e) {
			e.printStackTrace();
			accept = false;
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}

		if (accept) {
			spiralArms(clusterLine, starQuant / 4);
			createBulge();
			generateEmptyMap(new StructXY(0, 0), mapSize);
			augmentSpiralMap();
		}

	}

	/**
	 * Метод который генерирует спиральные рукава
	 * 
	 * @param clusterLine вектор из четырех кластеров в одномерных массивах
	 * @param starQuant количество звезд в одном рукаве
	 */
	private void spiralArms(Vector<HashMap<Integer, StructXY>> clusterLine, int starQuant) {
		MT19937 prng = new MT19937();

		/* Определяем направление вращения рукавов спирали. */
		int rotation = prng.nextInt(2);

		/* Определяем длину и ширину рукава спирали */
		int armLength = Arifmetic.roundDown(Math.sqrt(5 * starQuant));
		int armThickness = Arifmetic.roundUp(armLength / 5.0);

		/* Определяем шаг смещения по рукаву спирали */
		int shift = (int) Math.round(starQuant * 2.0 / armLength);

		/* Запуск цикла отрисовки рукавов спирали по кластерам */
		for (int i = 0; i <= 3; i++) {
			Vector<StructXY> curve = this.getSpiralCurve(clusterLine.get(i), rotation, i);
			StructXY startPos = null;
			int signX;
			int signY;

			/* Отрисовываем длину рукава */
			for (int count = 0; count < armLength; count++) {

				/* Начальная точка рукава */
				if (count == 0) {
					coordinateNet[curve.get(0).getX()][curve.get(0).getY()] = new Sector(curve.get(
						0).getX(), curve.get(0).getY(), true);
					startPos = curve.get(0);
				} else {

					/* Поиск следующей группы звезд в рукаве */
					for (StructXY element : curve) {
						if (this.getDistance(startPos.getX(), startPos.getY(), element.getX(),
							element.getY()) >= shift) {

							/*
							 * Реализация ширины рукава за счет повората
							 * плоскости локальный группы звезд относительно
							 * плоскости кривой
							 */
							signX = (int) Math.signum(element.getX() - startPos.getX());
							signY = (int) Math.signum(element.getY() - startPos.getY());
							if (signX == 0) {
								signX = 1;
							}
							if (signY == 0) {
								signY = 1;
							}

							for (int starThick = 0; starThick < armThickness; starThick++) {
								try {
									coordinateNet[element.getX() - signX
										* ((starThick - 1) * armThickness)][element
										.getY() + signY * ((starThick - 1) * armThickness)] = new Sector(
										element.getX() - signX
											* ((starThick - 1) * armThickness), element
											.getY() + signY * ((starThick - 1) * armThickness),
										true);
									startPos = element;
								} catch (ArrayIndexOutOfBoundsException e) {
									break;
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Метод для генерации балджа спиральной галактики(центр)
	 */
	private void createBulge() {
		int border = 0;
		int marginX = 0;
		int marginY = 0;

		/* Вычисляем координаты верхней левой точки балджа. */
		for (int i = 0; i < coordinateNet.length; i++) {
			if (coordinateNet[0][i] == null) {
				border++;
				if (border == 1) {
					marginY = i;
					for (int j = 0; j < coordinateNet.length; j++) {
						if (coordinateNet[j][marginX] == null) {
							marginX = j;
							break;
						}
					}
				}
			}
		}

		/* Запоняем балдж звездами. */
		this.randomUniverse(Math.round(border * border / 8),
			this.generateEmptyMap(new StructXY(marginX, marginY), border));
	}

	/**
	 * Метод для очистки спиральной карты от рядомстоящих звезд
	 * 
	 * @return количество удаленных звезд
	 */
	private int augmentSpiralMap() {
		Vector<StructXY> localSpace = new Vector<StructXY>();
		MT19937 prng = new MT19937();
		int deathStar = 0;

		for (int x = 0; x < coordinateNet.length; x++) {
			for (int y = 0; y < coordinateNet.length; y++) {

				/* Обнаруживаем звезду. */
				if (coordinateNet[x][y].getStar() != null) {

					/*
					 * Запускаем цикл который обходит ее по периметру и включает
					 * саму звезду.
					 */
					for (int i = 0; i < 3; i++) {
						for (int j = 0; j < 3; j++) {

							/* Проверяем что цикл не вышел за пределы карты. */
							try {
								coordinateNet[(x - 1 + i)][(y - 1 + j)].getName();

								/*
								 * Если по периметру обнаружена звезда помещаем
								 * ее в массив.
								 */
								if (coordinateNet[(x - 1 + i)][(y - 1 + j)].getStar() != null) {
									localSpace.add(new StructXY((x - 1 + i), (y - 1 + j)));
								}
							} catch (ArrayIndexOutOfBoundsException e) {
							};
						}
					}

					/*
					 * Убираем из списка "лишних" звезд одну, которая останется
					 * в этом регионе.
					 */
					localSpace.remove(prng.nextInt(localSpace.size()));

					/* Сектора с лишними звездами заменяем на пустые. */
					for (StructXY element : localSpace) {
						deathStar++;
						coordinateNet[element.getX()][element.getY()] = new Sector(element.getX(),
							element.getY(), false);
					}
					localSpace.clear();
				}
			}
		}

		return deathStar;
	}

	/**
	 * Метод для генерации звезд на карте кластерным способом
	 * 
	 * @param totalStarQuant всего звезд на карте
	 * @param numberOfClusters количество кластеров
	 */
	private void randomUniverse(int totalStarQuant, int numberOfClusters) {

		boolean accept = true;
		Vector<HashMap<Integer, StructXY>> clusterLine = null;
		try {
			clusterLine = this.clusterize(numberOfClusters, totalStarQuant);
		} catch (CantCreateException e) {
			e.printStackTrace();
			accept = false;
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}

		if (accept) {
			this.putStarsInClusters(numberOfClusters, totalStarQuant, clusterLine);
		}

	}

	/**
	 * Метод для заполнения указанного массива кластеров звездами по случайному
	 * принципу
	 * 
	 * @param numberOfClusters число кластеров для заполнения
	 * @param totalStarQuant общее количество звездных систем
	 * @param clusterLine вектор из кластеров в одномерных массивах
	 */
	private void putStarsInClusters(int numberOfClusters, int totalStarQuant,
		Vector<HashMap<Integer, StructXY>> clusterLine) {
		MT19937 prng = new MT19937();
		int position;
		int counter = 0;
		int starsInCluster = Math.round(totalStarQuant / numberOfClusters);

		/* Случайным образом выбираем кластеры для заполнения. */
		do {

			/* Определяем количество звезд в последнем кластере */
			if ((counter + 1) == numberOfClusters) {
				starsInCluster = totalStarQuant - starsInCluster * (numberOfClusters - 1);
			}

			position = prng.nextInt(clusterLine.size());
			randomUniverse(starsInCluster, clusterLine.get(position));
			clusterLine.remove(position);
			counter++;
		} while (counter < numberOfClusters);

	}

	/**
	 * Метод для генерации звездной карты с пустыми секторами
	 * 
	 * @param size размер карты
	 * @return возвращает одномерную карту
	 */
	private HashMap<Integer, StructXY> generateEmptyMap(StructXY startPos, int size) {
		HashMap<Integer, StructXY> planeUniverse = new HashMap<Integer, StructXY>();

		for (int i = startPos.getX(); i < (size + startPos.getX()); i++) {
			for (int j = startPos.getY(); j < size + (startPos.getY()); j++) {
				if ((coordinateNet[i][j] == null)) {
					coordinateNet[i][j] = new Sector(i, j, false);
				}
				planeUniverse.put(i * coordinateNet.length + j, new StructXY(i, j));
			}
		}

		return planeUniverse;
	}

	/**
	 * Метод который разбивает карту на кластеры и помещает их в вектор
	 * 
	 * @param numberOfClusters количество кластеров на которое необходимо
	 *        разбить карту
	 * @param totalStarQuant количество звездных систем для генерации
	 * @return
	 * @throws ExecutionException если невозможно сгенерить кластеры
	 * @throws IllegalAccessException если возникла ошибка при записи координат
	 *         в кластер
	 */
	private Vector<HashMap<Integer, StructXY>> clusterize(int numberOfClusters, int totalStarQuant)
		throws CantCreateException, IllegalAccessException {
		HashMap<Integer, StructXY> localPlaneUniverse = new HashMap<Integer, StructXY>();
		Vector<HashMap<Integer, StructXY>> clusterLine = new Vector<HashMap<Integer, StructXY>>();
		int border;
		int localPlanesSize;

		/* Получаем количество полуплоскостей разбивающих двумерную карту. */
		int halfPlanesQuant = Arifmetic.roundUp(Math.sqrt(numberOfClusters));

		/* Вычисляем начальное значение толщины границы */
		int calcBorder = Math.round(coordinateNet.length / 5);

		/*
		 * Получаем окончательное значение "толщины" границы разделяющей
		 * кластеры.
		 */
		if (((coordinateNet.length - (calcBorder) * (halfPlanesQuant - 1)) % halfPlanesQuant) == 0) {
			border = calcBorder;
		} else if (((coordinateNet.length - (calcBorder + 1) * (halfPlanesQuant - 1)) % halfPlanesQuant) == 0) {
			border = calcBorder + 1;
		} else {
			border = calcBorder - 1;
		}

		/* Получаем значение размера кластера */
		localPlanesSize = (int) ((coordinateNet.length - (border * (halfPlanesQuant - 1))) / halfPlanesQuant);

		/* Определяем возможность генерации указанного числа кластеров */
		int starsInCluster = Math.round(totalStarQuant / numberOfClusters);

		if ((localPlanesSize * localPlanesSize) < (5 * starsInCluster)) {
			throw new CantCreateException("Невозможно сгенерировать такое число звезд" +
				" для указанного числа кластеров");

		} else {

			/* Иницилизируем количество строк из кластеров. */
			int rowsCount = 0;

			/* Цикл, который помещает локальный кластер в вектор кластеров. */
			for (int numClust = 0; numClust < halfPlanesQuant * halfPlanesQuant; numClust++) {

				/* Определяем начальную точку кластера по вертикали. */
				int iDownLimit = rowsCount * (localPlanesSize + border);

				/* Определяем начальную точку кластера по горизонтали */
				int jDownLimit = (numClust - rowsCount * halfPlanesQuant)
					* (localPlanesSize + border);

				/*
				 * Вычисляем строку из кластеров, на которой в следующей
				 * итерации будет находиться цикл
				 */
				if ((numClust + 1) % halfPlanesQuant == 0) {
					rowsCount++;
				}

				/*
				 * Цикл который помещает координаты двумерного массива в
				 * кластер.
				 */
				for (int i = iDownLimit; i < iDownLimit + localPlanesSize; i++) {
					for (int j = jDownLimit; j < jDownLimit + localPlanesSize; j++) {
						coordinateNet[i][j] = new Sector(i, j, false);
						StructXY check = localPlaneUniverse.put(i * coordinateNet.length + j,
							new StructXY(i, j));
						if (check != null) {
							throw new IllegalAccessException(
								"Ошибка при записи координат в кластер");
						}
					}
				}

				/* Добавляем кластер в массив кластеров. */
				clusterLine.add(numClust, (HashMap<Integer, StructXY>) localPlaneUniverse.clone());

				/*
				 * Очищаем локальную карту после помещения ее копии в массив
				 * кластеров
				 */
				localPlaneUniverse.entrySet().clear();
			}
		}
		return clusterLine;
	}

	/**
	 * Метод генерирует случайное количество звезд на заданной одномерной карте
	 * 
	 * @param numOfStars количество звезд, которое необходимо создать
	 * @param planeUniverse одномерная карта
	 */
	private void randomUniverse(int numOfStars, HashMap<Integer, StructXY> planeUniverse) {
		int counter;
		counter = 0;

		MT19937 prng = new MT19937();
		do {
			/*
			 * Проверяем одномерную карту на наличие "свободных секторов", в
			 * случае их отсутствия прерываем генерацию звезд
			 */
			if (planeUniverse.isEmpty()) {
				System.out.println("Невозможно создать больше звезд на этой карте,"
					+ " чем уже создано " + counter);
				break;
			}

			int rnd = prng.nextInt(planeUniverse.size());

			/* Получаем координаты случайной "свободной" ячейки. */
			StructXY coord = (StructXY) planeUniverse.values().toArray()[rnd];

			int x = coord.getX();
			int y = coord.getY();

			coordinateNet[x][y] = new Sector(x, y, true);
			counter++;

			/*
			 * Убираем недопустимые для размещения звезд сектора из одномерной
			 * карты
			 */
			planeUniverseCleaner(x, y, planeUniverse);

		} while (counter < numOfStars);
	}

	/**
	 * Метод убирает недопустимые для размещения звезд сектора из одномерной
	 * карты
	 * 
	 * @param X координата X сектора где была размещена звезда
	 * @param Y координата Y сектора где была размещена звезда
	 * @param planeUniverse одномерная карта
	 */
	private void planeUniverseCleaner(int X, int Y,
		HashMap<Integer, StructXY> planeUniverse) {
		for (int i = 0; i < 3; i++) {
			for (int j = 0; j < 3; j++) {
				try {
					coordinateNet[(X - 1 + i)][(Y - 1 + j)].getName();
					planeUniverse.remove((X - 1 + i) * coordinateNet.length + Y - 1 + j);
				} catch (ArrayIndexOutOfBoundsException e) {
				} catch (NullPointerException ee) {
				};
			}
		}
	}

	/**
	 * Метод для получения координат кривой Безье в кластере
	 * 
	 * @param cluster массив координат кластера
	 * @param rotation направление вращения спирали
	 * @param clusterNum номер текущего кластера
	 * @return массив координат точек кривой
	 */
	private Vector<StructXY> getSpiralCurve(HashMap<Integer, StructXY> cluster, int rotation,
		int clusterNum) {
		int clusterSize = (int) Math.sqrt(cluster.size());

		/* Опрелеляем начальную опорную точку. */
		StructXY startPos = null;
		startPos = this.getMarginPoint(cluster);

		/* Определяем опорные точки кривой Безье. */
		StructXY firstXY = null;
		StructXY secondXY = null;
		StructXY thirdXY = null;
		StructXY forthXY = null;
		switch (clusterNum) {
			case 0: {
				firstXY = startPos;
				secondXY = new StructXY((int) (firstXY.getX() - Math.round(0.25 * clusterSize)),
					firstXY.getY() - clusterSize + 1);
				thirdXY = new StructXY((int) (secondXY.getX() - Math.round(0.5 * clusterSize) + 1),
					secondXY.getY());
				forthXY = new StructXY(thirdXY.getX(), (int) (thirdXY.getY()
					+ Math.round(clusterSize * 0.5) - 1));
				break;
			}
			case 1: {
				firstXY = startPos;
				secondXY = new StructXY((firstXY.getX() - clusterSize + 1),
					(int) (firstXY.getY() + Math.round(0.15 * clusterSize)));
				thirdXY = new StructXY((int) (secondXY.getX() + Math.round(0.5 * clusterSize)),
					secondXY.getY() + clusterSize - 1);
				forthXY = new StructXY((int) (thirdXY.getX() + Math.round(clusterSize * 0.5) - 1),
					thirdXY.getY());
				break;
			}
			case 2: {
				firstXY = startPos;
				secondXY = new StructXY(firstXY.getX() + clusterSize - 1,
					(int) (firstXY.getY() - Math.round(0.15 * clusterSize)));
				thirdXY = new StructXY(secondXY.getX(), (int) (secondXY.getY()
					- Math.round(0.75 * clusterSize) + 1));
				forthXY = new StructXY((int) (thirdXY.getX() - Math.round(0.5 * clusterSize) + 1),
					thirdXY.getY());
				break;
			}
			case 3: {
				firstXY = startPos;
				secondXY = new StructXY((int) (firstXY.getX() + Math.round(0.25 * clusterSize)),
					firstXY.getY() + clusterSize - 1);
				thirdXY = new StructXY((int) (secondXY.getX() + Math.round(0.5 * clusterSize) - 1),
					secondXY.getY());
				forthXY = new StructXY(thirdXY.getX(), (int) (thirdXY.getY()
					- Math.round(0.5 * clusterSize) + 1));
				break;
			}
		}

		/* Если вращение против часовой стрелки меняем опорные точки. */
		if (rotation == 1) {
			StructXY temp = forthXY;
			forthXY = secondXY;
			secondXY = temp;
		}

		/* Получаем массив точек сглаженной кубической кривой Безье. */
		Vector<StructXY> besyeXY = new Vector<StructXY>();
		for (double t = 0; t <= 1; t = t + 0.02) {
			int x = (int) Math.round(Math.pow((1 - t), 3) * firstXY.getX() + 3 * t
				* Math.pow((1 - t), 2) * secondXY.getX() + 3 * t * t * (1 - t) * thirdXY.getX() + t
				* t * t * forthXY.getX());
			int y = (int) Math.round(Math.pow((1 - t), 3) * firstXY.getY() + 3 * t
				* Math.pow((1 - t), 2) * secondXY.getY() + 3 * t * t * (1 - t) * thirdXY.getY() + t
				* t * t * forthXY.getY());
			besyeXY.add(new StructXY(x, y));
		}

		return besyeXY;

	}

	/**
	 * Метод для получения точки кластера максимально приближенной к балджу
	 * 
	 * @param cluster массив точек кластера
	 * @return ближайшую к центру точку кластера
	 */
	private StructXY getMarginPoint(HashMap<Integer, StructXY> cluster) {
		StructXY startPos = null;
		int check;
		for (StructXY element : cluster.values()) {
			check = 0;
			try {
				if (coordinateNet[element.getX() + 1][element.getY()] == null) {
					check++;
				}
				if ((coordinateNet[element.getX() - 1][element.getY()] == null)) {
					check++;
				}
				if (coordinateNet[element.getX()][element.getY() + 1] == null) {
					check++;
				}
				if (coordinateNet[element.getX()][element.getY() - 1] == null) {
					check++;
				}
			} catch (ArrayIndexOutOfBoundsException e) {
			}
			if (check == 2) {
				startPos = element;
				break;
			}
		}
		return startPos;
	}

	/**
	 * Метод для определения расстояния между секторами
	 * 
	 * @param x1 координата X первой точки
	 * @param y1 координата Y первой точки
	 * @param x2 координата X второй точки
	 * @param y2 координата Y второй точки
	 * @return Расстояние в световых годах
	 */
	public int getDistance(int x1, int y1, int x2, int y2) {
		return Arifmetic.roundDown(Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2)));
	}

	/* Методы доступа к защищенным данным класса. */

	/**
	 * @return the coordinateNet
	 */
	public Sector[][] getCoordinateNet() {
		return this.coordinateNet;
	}

}
